热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

尾部|柜台_Java并发线程池篇附场景分析

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。作者:汤圆个人博客

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。


作者:汤圆

个人博客:javalover.cc


前言

前面我们在创建线程时,都是直接new Thread();

这样短期来看是没有问题的,但是一旦业务量增长,线程数过多,就有可能导致内存异常OOM,CPU爆满等问题

幸运的是,Java里面有线程池的概念,而线程池的核心框架,就是我们今天的主题,Executor

接下来,就让我们一起畅游在Java线程池的海洋中吧



本节会用银行办业务的场景来对比介绍线程池的核心概念,这样理解起来会很轻松



简介

Executor是线程池的核心框架;

和它相对应的有一个辅助工厂类Executors,这个类提供了许多工厂方法,用来创建各种各样的线程池,下面我们先看下几种常见的线程池

// 容量固定的线程池
Executor fixedThreadPool = Executors.newFixedThreadPool(5);
// 容量动态增减的线程池
Executor cachedThreadPool = Executors.newCachedThreadPool();
// 单个线程的线程池
Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
// 基于调度机制的线程池(不同于上面的线程池,这个池创建的任务不会立马执行,而是定期或者延时执行)
Executor scheduledThreadPool = Executors.newScheduledThreadPool(5);

上面这些线程池的区别主要就是线程数量的不同以及任务执行的时机

下面让我们开始吧



文章如果有问题,欢迎大家批评指正,在此谢过啦



目录


  1. 线程池的底层类ThreadPoolExecutor
  2. 为啥阿里不建议使用 Executors来创建线程池?
  3. 线程池的生命周期 ExecutorService

正文


1. 线程池的底层类 ThreadPoolExecutor

在文章开头创建的几个线程池,内部都是有调用ThreadPoolExecutor这个类的,如下所示

public static ExecutorService newFixedThreadPool(int nThreads)
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());

这个类是Exexutor的一个实现类&#xff0c;关系图如下所示&#xff1a;


  • 其中Executors就是上面介绍的辅助工厂类&#xff0c;用来创建各种线程池

  • 接口ExecutorService是Executor的一个子接口&#xff0c;它对Executor进行了扩展&#xff0c;原有的Executor只能执行任务&#xff0c;而ExecutorService还可以管理线程池的生命周期&#xff08;下面会介绍&#xff09;

所以我们先来介绍下这个底层类&#xff0c;它的完整构造参数如下所示&#xff1a;

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

在介绍这些参数之前&#xff0c;我们可以先举个生活中的例子-去银行办业务&#xff1b;然后对比着来理解&#xff0c;会比较清晰

&#xff08;图中绿色的窗口表示一直开着&#xff09;


  • corePoolSize&#xff1a; 核心线程数&#xff0c;就是一直存在的线程&#xff08;不管用不用&#xff09;&#xff1b;&#61;》窗口的1号窗和2号窗
  • maximumPoolSize&#xff1a;最大线程数&#xff0c;就是最多可以创建多少个线程&#xff1b;&#61;》窗口的1&#xff0c;2&#xff0c;3&#xff0c;4号窗
  • keepAliveTime&#xff1a;多余的线程&#xff08;最大线程数 减去 核心线程数&#xff09;空闲时存活的时间&#xff1b;&#61;》窗口的3号窗和4号窗空闲的时间,如果超过keepAliveTime&#xff0c;还没有人来办业务&#xff0c;那么就会暂时关闭3号窗和4号窗
  • workQueue: 工作队列&#xff0c;当核心线程数都在执行任务时&#xff0c;再进来的任务就会添加到工作队列中&#xff1b;&#61;》椅子&#xff0c;客户等待区
  • threadFactory&#xff1a;线程工厂&#xff0c;用来创建初始的核心线程&#xff0c;下面会有介绍&#xff1b;
  • handler&#xff1a;拒绝策略&#xff0c;当所有线程都在执行任务&#xff0c;且工作队列也满时&#xff0c;再进来的任务就会被执行拒绝策略&#xff08;比如丢弃&#xff09;&#xff1b;&#61;》左下角的那个小人

基本的工作流程如下所示&#xff1a;

上面的参数我们着重介绍下工作队列和拒绝策略&#xff0c;线程工厂下面再介绍

工作队列&#xff1a;


  • ArrayBlockingQueue&#xff1a;
    • 数组阻塞队列&#xff0c;这个队列是一个有界队列&#xff0c;遵循FIFO&#xff0c;尾部插入&#xff0c;头部获取
    • 初始化时需指定队列的容量 capacity
    • 类比到上面的场景&#xff0c;就是椅子的数量为初始容量capacity
  • LinkedBlockingQueue&#xff1a;
    • 链表阻塞队列&#xff0c;这是一个无界队列&#xff0c;遵循FIFO&#xff0c;尾部插入&#xff0c;头部获取
    • 初始化时可不指定容量&#xff0c;此时默认的容量为Integer.MAX_VALUE&#xff0c;基本上相当于无界了&#xff0c;此时队列可一直插入&#xff08;如果处理任务的速度小于插入的速度&#xff0c;时间长了就有可能导致OOM)
    • 类比到上面的场景&#xff0c;就是椅子的数量为Integer.MAX_VALUE
  • SynchronousQueue&#xff1a;
    • 同步队列&#xff0c;阻塞队列的特殊版&#xff0c;即没有容量的阻塞队列&#xff0c;随进随出&#xff0c;不做停留
    • 类比到上面的场景&#xff0c;就是椅子的数量为0&#xff0c;来一个人就去柜台办理&#xff0c;如果柜台满了&#xff0c;就拒绝
  • PriorityBlockingQueue
    • 优先级阻塞队列&#xff0c;这是一个无界队列&#xff0c;不遵循FIFO&#xff0c;而是根据任务自身的优先级顺序来执行
    • 初始化可不指定容量&#xff0c;默认11&#xff08;既然有容量&#xff0c;怎么还是无界的呢&#xff1f;因为它添加元素时会进行扩容&#xff09;
    • 类比到上面的场景&#xff0c;就是新来的可以插队办理业务&#xff0c;好比各种会员

拒绝策略&#xff1a;


  • AbortPolicy&#xff08;默认&#xff09;&#xff1a;
    • 中断策略&#xff0c;抛出异常 RejectedExecutionException&#xff1b;
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则抛出 RejectedExecutionException&#xff08;系统会停止运行&#xff0c;但是不会退出&#xff09;
  • DiscardPolicy&#xff1a;
    • 丢弃策略&#xff0c;丢掉新来的任务
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接丢掉&#xff08;看任务的重要程度&#xff0c;不重要的任务可以用这个策略&#xff09;
  • DiscardOldestPolicy&#xff1a;
    • 丢弃最旧策略&#xff0c;丢掉最先进入队列的任务&#xff08;有点残忍了&#xff09;&#xff0c;然后再次执行插入操作
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接丢掉队列头部的任务&#xff0c;并再次插入任务
  • CallerRunsPolicy&#xff1a;
    • 回去执行策略&#xff0c;让新来的任务返回到调用它的线程中去执行&#xff08;比如main线程调用了executors.execute(task)&#xff0c;那么就会将task返回到main线程中去执行&#xff09;
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接返回该任务&#xff0c;到调用它的线程中去执行

2. 为啥阿里不建议使用 Executors来创建线程池&#xff1f;

原话如下&#xff1a;

我们可以写几个代码来测试一下

先测试FixedThreadPool&#xff0c;代码如下&#xff1a;

public class FixedThreadPoolDemo
public static void main(String[] args)
// 创建一个固定容量为10的线程池&#xff0c;核心线程数和最大线程数都为10
ExecutorService executorService &#61; Executors.newFixedThreadPool(10);
for (int i &#61; 0; i < 1_000_000; i&#43;&#43;)
try
executorService.execute(()->
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();

);
catch (Exception e)
e.printStackTrace();




这里我们需对VM参数做一点修改&#xff0c;让问题比较容易复现

如下所示&#xff0c;我们添加-Xmx8m -Xms8m到VM option中(-Xmx8m&#xff1a;JVM堆的最大内存为8M&#xff0c; -Xms8m&#xff0c;JVM堆的初始化内存为8M)&#xff1a;

此时点击运行&#xff0c;就会发现报错如下&#xff1a;

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.jalon.concurrent.chapter6.FixedThreadPoolDemo.main(FixedThreadPoolDemo.java:21)

我们来分析下原因


  • 首先&#xff0c;newFixedThreadPool内部用的工作队列为LinkedBlockingQueue&#xff0c;这是一个无界队列&#xff08;容量最大为Integer.MAX_VALUE&#xff0c;基本上可一直添加任务&#xff09;
  • 如果任务插入的速度&#xff0c;超过了任务执行的速度&#xff0c;那么队列肯定会越来越长&#xff0c;最终导致OOM


CachedThreadPool也是类似的原因&#xff0c;只不过它是因为最大线程数为Integer.MAX_VALUE&#xff1b;


所以当任务插入的速度&#xff0c;超过了任务执行的速度&#xff0c;那么线程的数量会越来越多&#xff0c;最终导致OOM


那我们要怎么创建线程池呢&#xff1f;

可以用ThreadPoolExecutor来自定义创建&#xff0c;通过为最大线程数和工作队列都设置一个边界&#xff0c;来限制相关的数量&#xff0c;如下所示&#xff1a;

public class ThreadPoolExecutorDemo
public static void main(String[] args)
ExecutorService service &#61; new ThreadPoolExecutor(
1, // 核心线程数
1, // 最大线程数
60L, // 空闲时间
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1), // 数组工作队列&#xff0c;长度1
new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略&#xff1a;丢弃
for (int i &#61; 0; i < 1_000_000; i&#43;&#43;)
// 通过这里的打印信息&#xff0c;我们可以知道循环了3次
// 原因就是第一次的任务在核心线程中执行&#xff0c;第二次的任务放到了工作队列&#xff0c;第三次的任务被拒绝执行
System.out.println(i);
service.execute(()->
// 这里会报异常&#xff0c;是因为执行了拒绝策略&#xff08;达到了最大线程数&#xff0c;队列也满了&#xff0c;此时新进来的任务就会执行拒绝策略&#xff09;
// 这里需要注意的是&#xff0c;抛出异常后&#xff0c;代码并不会退出&#xff0c;而是卡在异常这里&#xff0c;包括主线程也会被卡住(这个是默认的拒绝策略&#xff09;
// 我们可以用其他的拒绝策略&#xff0c;比如DiscardPolicy,此时代码就会继续往下执行
System.out.println(Thread.currentThread().getName());
);

try
Thread.sleep(1000);
System.out.println("主线程 sleep ");
catch (InterruptedException e)
e.printStackTrace();




3. 线程池的生命周期 ExecutorService

Executor接口默认只有一个方法void execute(Runnable command);&#xff0c;用来执行任务

任务一旦开启&#xff0c;我们就无法再去插手了&#xff0c;比如停止、监控等

此时就需要ExecutorService登场了&#xff0c;它是Executor的一个子接口&#xff0c;对其进行了扩展&#xff0c;方法如下&#xff1a;

public interface ExecutorService extends Executor
void shutdown(); // 优雅地关闭&#xff0c;这个关闭会持续一段时间&#xff0c;以等待已经提交的任务去执行完成&#xff08;但是在shutdown之后提交的任务会被拒绝&#xff09;
List<Runnable> shutdownNow(); // 粗暴地关闭&#xff0c;这个关闭会立即关闭所有正在执行的任务&#xff0c;并返回工作队列中等待的任务
boolean isShutdown();
boolean isTerminated();
// 用来等待线程的执行
// 如果在timeout之内&#xff0c;线程都执行完了&#xff0c;则返回true&#xff1b;
// 如果等了timeout&#xff0c;还没执行完&#xff0c;则返回false&#xff1b;
// 如果timeout之内&#xff0c;线程被中断&#xff0c;则抛出中断异常
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

从上面可以看到&#xff0c;线程池的生命周期分三步&#xff1a;


  1. 运行&#xff1a;创建后就开始运行
  2. 关闭&#xff1a;调用shutdown进入关闭状态
  3. 已终止&#xff1a;所有线程执行完毕

总结


  1. 线程池的底层类 ThreadPoolExecutor&#xff1a;核心概念就是核心线程数、最大线程数、工作队列、拒绝策略
  2. 为啥阿里不建议使用 Executors来创建线程池&#xff1f;&#xff1a;因为会导致OOM&#xff0c;解决办法就是自定义ThreadPoolExecutor&#xff0c;为最大线程数和工作队列设置边界
  3. 线程池的生命周期ExecutorService&#xff1a;运行状态&#xff08;创建后进入&#xff09;、关闭状态&#xff08;shutdown后进入&#xff09;、已终止状态&#xff08;所有线程都执行完成后进入&#xff09;

参考内容&#xff1a;


  • 《Java并发编程实战》
  • 《实战Java高并发》
  • newFixedThreadPool的弊端&#xff1a;https://my.oschina.net/langwanghuangshifu/blog/3208320
  • 银行办业务的场景参考&#xff1a;https://b23.tv/ygGjTH

后记

愿你的意中人亦是中意你之人


推荐阅读
  • 来自FallDream的博客,未经允许,请勿转载,谢谢。一天一套noi简直了.昨天勉强做完了noi2011今天教练又丢出来一套noi ... [详细]
  • LeetCode 102 - 二叉树层次遍历详解
    本文详细解析了LeetCode第102题——二叉树的层次遍历问题,提供了C++语言的实现代码,并对算法的核心思想和具体步骤进行了深入讲解。 ... [详细]
  • 流处理中的计数挑战与解决方案
    本文探讨了在流处理中进行计数的各种技术和挑战,并基于作者在2016年圣何塞举行的Hadoop World大会上的演讲进行了深入分析。文章不仅介绍了传统批处理和Lambda架构的局限性,还详细探讨了流处理架构的优势及其在现代大数据应用中的重要作用。 ... [详细]
  • 本文详细介绍了Socket在Linux内核中的实现机制,包括基本的Socket结构、协议操作集以及不同协议下的具体实现。通过这些内容,读者可以更好地理解Socket的工作原理。 ... [详细]
  • 解决Expo XDE 2.22.1版本启动错误
    根据问题描述,用户在将Expo升级至2.22.1版本后,在尝试打开项目时遇到了错误。本文提供了详细的错误分析及解决方案。 ... [详细]
  • 本文详细介绍了个人电脑的组装流程,从基础的机箱准备到复杂的系统安装,每一步都提供了详细的指导,旨在帮助DIY爱好者顺利完成自己的电脑组装。 ... [详细]
  • 题目描述:Balala Power! 时间限制:4000/2000 MS (Java/Other) 内存限制:131072/131072 K (Java/Other)。题目背景及问题描述详见正文。 ... [详细]
  • 一家位于长沙的知名网络安全企业,现面向全国诚聘高级后端开发工程师,特别欢迎具有一线城市经验的技术精英回归故乡,共创辉煌。 ... [详细]
  • 本文探讨了异步编程的发展历程,从最初的AJAX异步回调到现代的Promise、Generator+Co以及Async/Await等技术。文章详细分析了Promise的工作原理及其源码实现,帮助开发者更好地理解和使用这一重要工具。 ... [详细]
  • 洛谷 P4009 汽车加油行驶问题 解析
    探讨了经典算法题目——汽车加油行驶问题,通过网络流和费用流的视角,深入解析了该问题的解决方案。本文将详细阐述如何利用最短路径算法解决这一问题,并提供详细的代码实现。 ... [详细]
  • 本篇文章详细探讨了微机原理实验中的指令系统,特别是第三章的内容。对于希望深入了解微机工作原理和技术实现的朋友来说,这是一篇不可多得的技术指南。文章不仅涵盖了基础概念,还深入讲解了指令格式、操作数类型以及各种寻址方式,旨在帮助读者更好地掌握微机指令系统的应用。 ... [详细]
  • 管理UINavigationController中的手势返回 - Managing Swipe Back Gestures in UINavigationController
    本文介绍了如何在一个简单的闪存卡片应用中实现平滑的手势返回功能,以增强用户体验。 ... [详细]
  • 本文深入探讨了Linux内核中进程地址空间的设计与实现,包括虚拟地址空间的概念、内存描述符`mm_struct`的作用、内核线程与用户进程的区别、进程地址空间的分配方法、虚拟内存区域(VMA)的结构以及地址空间与页表之间的映射机制。 ... [详细]
  • 吴石访谈:腾讯安全科恩实验室如何引领物联网安全研究
    腾讯安全科恩实验室曾两次成功破解特斯拉自动驾驶系统,并远程控制汽车,展示了其在汽车安全领域的强大实力。近日,该实验室负责人吴石接受了InfoQ的专访,详细介绍了团队未来的重点方向——物联网安全。 ... [详细]
  • 七大策略降低云上MySQL成本
    在全球经济放缓和通胀压力下,降低云环境中MySQL数据库的运行成本成为企业关注的重点。本文提供了一系列实用技巧,旨在帮助企业有效控制成本,同时保持高效运作。 ... [详细]
author-avatar
子新宥梅93
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有